python 2.6.6 SimpleHTTPServer耗时的bug

之前因为时间紧迫的缘故,在公司的CentOS 7 机器上部署一个快速实现的数据收集工具,考虑到避免其他语言发布和变更起来麻烦,于是就用 python+bash 实现。

服务端就是最简单不引入任何python包的SimpleHTTPServer+Handler实现的。

偶然一次看日志发现本来应该在三分钟内完成的一个同步/上报循环结果未能如期完成,每个请求看起来相隔十秒左右,最后定位下来是python2.6.6的问题。

简单起见,我用下面重现一下: 10.135.20.45上开了一个进程

python -m SimpleHTTPServer 12888

在另外一台机器上

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
[java@10-135-20-62 ~]$ cat curl-format.txt
time_namelookup: %{time_namelookup}\n
time_connect: %{time_connect}\n
time_appconnect: %{time_appconnect}\n
time_redirect: %{time_redirect}\n
time_pretransfer: %{time_pretransfer}\n
time_starttransfer: %{time_starttransfer}\n
----------time_total: %{time_total}\n
[java@10-135-20-62 ~]$ curl -w "@curl-format.txt" -o /dev/null -s -L "10.135.20.45:12888"
time_namelookup: 0.000
time_connect: 0.000
time_appconnect: 0.000
time_redirect: 0.000
time_pretransfer: 0.000
time_starttransfer: 10.013
----------time_total: 10.013

这里两个问题:
1)一个简单的GET请求耗时10秒,10秒看起来像是服务器设置的超时时间。
2)而且10秒都花费在 starttransfer

如何定位问题?显然逐步打点耗时是可以的,但是如果用搜索引擎更快,我在python社区上找到一个提问,作者遇到同样现象,但是pytohn2.7以后就没有问题。

有个回答说是 pytohn解析header时候\r\n的问题,也有说是socket未close问题,我看了下代码,不像。

不过这个提问给了我思路,于是我比较了python 2.6/2.7各自的SimpleHTTPServer.py文件。
考虑到linux版本众多且你可能也记不住,友情提示一个定位该文件的方法:

1
2
3
4
[java@10-135-20-62 ~]$ python --version
Python 2.6.6
[java@10-135-20-62 ~]$ python -c 'import sys; print sys.path'
['', '/usr/lib64/python26.zip', '/usr/lib64/python2.6', '/usr/lib64/python2.6/plat-linux2', '/usr/lib64/python2.6/lib-tk', '/usr/lib64/python2.6/lib-old', '/usr/lib64/python2.6/lib-dynload', '/usr/lib64/python2.6/site-packages', '/usr/lib64/python2.6/site-packages/gtk-2.0', '/usr/lib/python2.6/site-packages', '/usr/lib/python2.6/site-packages/setuptools-0.6c11-py2.6.egg-info']

一般就在 /usr/lib64/python2.6/ 下面了。
不过2.6/2.7的SimpleHTTPServer.py没有明显差异,继续再比对他们的父类:BaseHTTPServer.py
下图简单列下不同点。



时间问题,直接上答案,问题就出在log_message里,python原意只是记录response的相关结果,但没想到会成为耗时的元凶:

1
2
3
4
5
6
7
def address_string(self):
"""Return the client address formatted for logging.
This version looks up the full hostname using gethostbyaddr(),
and tries to find a name that contains at least one dot.
"""
host, port = self.client_address[:2]
return socket.getfqdn(host)

我在该机器上跑一下,发现却是是耗费10秒

time python -c 'import socket; print socket.getfqdn()'

python 2.7 之后就改过来了,不再适用 address_string,所以在现代服务器上不会出现上述问题。

getfqdn为什么会耗时呢?这和pyhon hostname 解析实现相关,后续可分享一下。

以上简单记录遇到的问题,读者可留意。解决方法其实可以替换python的BaseHTTPServer.py,也可以自己拷贝出来一份修改,但说到底不如正规的方案去实现。

后记:

getfqdn 底层调用的就是 socket.gethostbyaddr() – map an IP number or hostname to DNS info
(生产环境配置 域名/反向域名解析服务)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
[java@10-135-20-62 ~]$ time python -c 'import socket; print socket.gethostbyaddr("10.135.20.62")'
('localhost', ['localhost.localdomain', 'localhost4', 'localhost4.localdomain4'], ['10.135.20.62'])
real 0m0.019s
user 0m0.014s
sys 0m0.004s
[java@10-135-20-62 ~]$ time python -c 'import socket; print socket.gethostbyaddr("10.135.20.39")'
Traceback (most recent call last):
File "<string>", line 1, in <module>
socket.herror: [Errno 2] Host name lookup failure
real 0m10.033s
user 0m0.018s
sys 0m0.004s

socket.py

1
2
3
4
5
[java@10-135-20-62 ~]$ python -c 'import _socket;print(_socket.__file__)'
/usr/lib64/python2.6/lib-dynload/_socketmodule.so
[java@10-135-20-62 ~]$ nm -D /usr/lib64/python2.6/lib-dynload/_socketmodule.so
[java@10-135-20-62 ~]$ objdump -tT /usr/lib64/python2.6/lib-dynload/_socketmodule.so
...